﻿; 自定义鼠标事件
; 需要注意的是：注册的所有事件回调在每次触发时都会调用，所以需要仔细的做前置判断，以此避免不需要的调用
; 同时，只会捕获窗口内的鼠标事件。如果要捕获全局事件，参考下面的代码：
;
; hHookMouse := DllCall("SetWindowsHookEx", "int", 14, "Uint", CallbackCreate(MouseCB, "Fast"), "Uint", 0, "Uint", 0)
; MouseCB(nCode, wParam, lParam) {
;   local mouseX := NumGet(lParam, 0, "int")
;   local mouseY := NumGet(lParam, 4, "int")
;   Tooltip mouseX '-' mouseY
;   Return DllCall("CallNextHookEx", "Uint", 0, "int", nCode, "Uint", wParam, "Uint", lParam)
; }
; OnExit((*) => DllCall("UnhookWindowsHookEx", "Uint", hHookMouse))

#Include ../../Extend.ahk

class MouseEvent {

  class Events {
    static LBUTTON_DOWN := 0x0201,
      LBUTTON_UP := 0x0202,
      LBUTTON_DBLCLK := 0x0203,
      RBUTTON_DOWN := 0x0204,
      RBUTTON_UP := 0x0205,
      MBUTTON_DOWN := 0x0207,
      MBUTTON_UP := 0x0208,
      MOUSE_MOVE := 0x0200,
      MOUSE_WHEEL := 0x020A,
      MOUSE_WHEEL_UP := 0x020B, ; 拆成两个事件
      MOUSE_WHEEL_DOWN := 0x20C
  }

  _events := Map()

  __New(hwnd) {
    this.hwnd := hwnd
    OnMessage(MouseEvent.Events.LBUTTON_DOWN, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.LBUTTON_UP, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.LBUTTON_DBLCLK, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.RBUTTON_DOWN, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.RBUTTON_UP, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.MBUTTON_DOWN, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.MBUTTON_UP, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.MOUSE_MOVE, (P*) => this._HandleEvent(p*))
    OnMessage(MouseEvent.Events.MOUSE_WHEEL, (P*) => this._HandleEvent(p*))
  }

  __Get(Key, Params) => this[key]

  _HandleEvent(wp, lp, msg, hwnd) {
    if hwnd != this.hwnd
      return
    switch msg { ; 细节请参阅 https://learn.microsoft.com/zh-cn/windows/win32/inputdev/mouse-input-notifications
      ; 正常情况下返回空即可
      case MouseEvent.Events.LBUTTON_DOWN:
        point := Pos(lp & 0xFFFF, lp >> 16), this.LD := true, this.LDPoint := point ; TODO 获取修饰符
        _CallCB(msg, point)
      case MouseEvent.Events.LBUTTON_UP:
        point := Pos(lp & 0xFFFF, lp >> 16), this.LD := false, this.LUPoint := point
        _CallCB(msg, point)
      case MouseEvent.Events.LBUTTON_DBLCLK: ; FIXME 始终触发单击
        point := Pos(lp & 0xFFFF, lp >> 16)
        _CallCB(msg, point)
        _CallCB(MouseEvent.Events.LBUTTON_DOWN, point) ; TODO 设置条件
      case MouseEvent.Events.MOUSE_MOVE:
        static lx := 0, ly := 0
        x := lp & 0xFFFF, y := lp >> 16
        if lx = x and ly = y
          return
        lx := x, ly := y, point := Pos(x, y)
        _CallCB(MouseEvent.Events.MOUSE_MOVE, point)
      case MouseEvent.Events.RBUTTON_DOWN:
        point := Pos(lp & 0xFFFF, lp >> 16), this.RD := true, this.RDPoint := point
        _CallCB(msg, point)
      case MouseEvent.Events.RBUTTON_UP:
        point := Pos(lp & 0xFFFF, lp >> 16), this.RD := false, this.RUPoint := point
        _CallCB(msg, point)
      case MouseEvent.Events.MBUTTON_DOWN:
        point := Pos(lp & 0xFFFF, lp >> 16), this.MD := true, this.MDPoint := point
        _CallCB(msg, point)
      case MouseEvent.Events.MBUTTON_UP:
        point := Pos(lp & 0xFFFF, lp >> 16), this.MD := false, this.MUPoint := point
        _CallCB(msg, point)
      case MouseEvent.Events.MOUSE_WHEEL:
        up := (wp >> 16 = 120), point := Pos(lp & 0xFFFF, lp >> 16) ; 屏幕坐标，实际使用需要转换成工作区坐标
        _CallCB(msg, point, up)
        _CallCB(up ? MouseEvent.Events.MOUSE_WHEEL_UP : MouseEvent.Events.MOUSE_WHEEL_DOWN, point)
      default:
    }

    _CallCB(event, params*) {
      if this._events.Has(event)
        this._events.Get(event).foreach(v => v(params*))
    }
  }

  Register(event, cb) {
    if !this._events.Has(event)
      this._events.Set(event, [])
    this._events.Get(event).Push(cb)
  }

  RegisterOnCondition(event, condition, cb1, cb2 := Noop) {
    this.Register(event, (p*) => condition(p*) ? cb1(p*) : cb2(p*))
  }
}

class Pos {

  __New(x, y) {
    this.x := x, this.y := y
  }

  Equals(pos1) {
    return this.x = pos1.x && this.y = pos1.y
  }

  InRect(x, y, w, h) => this.x.between(x, x + w) && this.y.between(y, y + h)

  ConvertToScreen(win) {
    win.GetClientPos(&x, &y)
    return {
      x: this.x + x,
      y: this.y + y
    }
  }

  ConvertToClient(win) {
    win.GetClientPos(&x, &y)
    return {
      x: Abs(x - this.x),
      y: Abs(y - this.y)
    }
  }
}